defmodule ZiglerTest.OptionsTest do
  use ExUnit.Case, async: true

  defp make_module(opts), do: Zig.Module.new(opts ++ [nifs: [:foo], language: Elixir], __ENV__)

  describe "for the module_code_path option" do
    test "string is ok" do
      assert %Zig.Module{} = make_module(otp_app: :zigler, module_code_path: "foobar")
    end

    test "iodata is ok" do
      assert %Zig.Module{} = make_module(otp_app: :zigler, module_code_path: ["foo" | "bar"])
    end

    test "non-path raises" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `module_code_path` must be a path, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, module_code_path: :foo)
                   end
    end
  end

  describe "for the zig_code_path option" do
    test "string is ok" do
      assert %Zig.Module{} = make_module(otp_app: :zigler, zig_code_path: "foobar")
    end

    test "iodata is ok" do
      assert %Zig.Module{} = make_module(otp_app: :zigler, zig_code_path: ["foo" | "bar"])
    end

    test "non-path raises" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `zig_code_path` must be a path, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, zig_code_path: :foo)
                   end
    end
  end

  describe "for c option" do
    test "invalid subkey" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c` was supplied the invalid option `foo`",
                   fn ->
                     make_module(otp_app: :zigler, c: [foo: :bar])
                   end
    end

    test "include_dirs accepts various forms" do
      local_dir = Path.join(__DIR__, "my_dir")

      assert %{c: %{include_dirs: [^local_dir]}} =
               make_module(otp_app: :zigler, c: [include_dirs: "my_dir"])

      assert %{c: %{include_dirs: [^local_dir]}} =
               make_module(otp_app: :zigler, c: [include_dirs: ~c'my_dir'])

      priv_dir = Path.join(:code.priv_dir(:zigler), "my_dir")

      assert %{c: %{include_dirs: [^priv_dir]}} =
               make_module(otp_app: :zigler, c: [include_dirs: {:priv, "my_dir"}])

      assert %{c: %{include_dirs: [system: "system"]}} =
               make_module(otp_app: :zigler, c: [include_dirs: {:system, "system"}])

      assert %{c: %{include_dirs: [^local_dir, ^priv_dir, system: "system"]}} =
               make_module(
                 otp_app: :zigler,
                 c: [include_dirs: ["my_dir", priv: "my_dir", system: "system"]]
               )
    end

    @valid_type_phrase "path, `{:priv, path}`, `{:system, path}`, or a list of those"

    test "include_dirs not a string list" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > include_dirs` must be #{@valid_type_phrase}, got: `:moop`",
                   fn ->
                     make_module(otp_app: :zigler, c: [include_dirs: :moop])
                   end
    end

    test "include_dirs priv not iodata" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > include_dirs` priv path must be iodata, got: `:foo`",
                   fn -> make_module(otp_app: :zigler, c: [include_dirs: [priv: :foo]]) end
    end

    test "include_dirs system not a string" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > include_dirs` system path must be iodata, got: `:foo`",
                   fn -> make_module(otp_app: :zigler, c: [include_dirs: [system: :foo]]) end
    end

    test "library_dirs accepts various forms" do
      local_dir = Path.join(__DIR__, "my_dir")

      assert %{c: %{library_dirs: [^local_dir]}} =
               make_module(otp_app: :zigler, c: [library_dirs: "my_dir"])

      priv_dir = Path.join(:code.priv_dir(:zigler), "my_dir")

      assert %{c: %{library_dirs: [^priv_dir]}} =
               make_module(otp_app: :zigler, c: [library_dirs: {:priv, "my_dir"}])

      assert %{c: %{library_dirs: [system: "system"]}} =
               make_module(otp_app: :zigler, c: [library_dirs: {:system, "system"}])

      assert %{c: %{library_dirs: [^local_dir, ^priv_dir, system: "system"]}} =
               make_module(
                 otp_app: :zigler,
                 c: [library_dirs: ["my_dir", priv: "my_dir", system: "system"]]
               )
    end

    test "library_dirs not a string list" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > library_dirs` must be #{@valid_type_phrase}, got: `:moop`",
                   fn ->
                     make_module(otp_app: :zigler, c: [library_dirs: :moop])
                   end
    end

    test "link_lib accepts various forms" do
      local_dir = Path.join(__DIR__, "my_dir")

      assert %{c: %{link_lib: [^local_dir]}} =
               make_module(otp_app: :zigler, c: [link_lib: "my_dir"])

      priv_dir = Path.join(:code.priv_dir(:zigler), "my_dir")

      assert %{c: %{link_lib: [^priv_dir]}} =
               make_module(otp_app: :zigler, c: [link_lib: {:priv, "my_dir"}])

      assert %{c: %{link_lib: [system: "system"]}} =
               make_module(otp_app: :zigler, c: [link_lib: {:system, "system"}])

      assert %{c: %{link_lib: [^local_dir, ^priv_dir, system: "system"]}} =
               make_module(
                 otp_app: :zigler,
                 c: [link_lib: ["my_dir", priv: "my_dir", system: "system"]]
               )
    end

    test "link_lib not a string list" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > link_lib` must be #{@valid_type_phrase}, got: `:moop`",
                   fn ->
                     make_module(otp_app: :zigler, c: [link_lib: :moop])
                   end
    end

    test "link_libcpp not a boolean" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > link_libcpp` must be a boolean, got: `:moop`",
                   fn ->
                     make_module(otp_app: :zigler, c: [link_libcpp: :moop])
                   end
    end

    test "src accepts basic string forms" do
      local_file = Path.join(__DIR__, "my_file.c")

      assert %{c: %{src: [{^local_file, []}]}} =
               make_module(otp_app: :zigler, c: [src: "my_file.c"])

      assert %{c: %{src: [{^local_file, []}]}} =
               make_module(otp_app: :zigler, c: [src: ~c'my_file.c'])
    end

    test "src accepts string with string options" do
      local_dir = Path.join(__DIR__, "my_dir")

      assert %{c: %{src: [{^local_dir, ["foo", "bar"]}]}} =
               make_module(otp_app: :zigler, c: [src: {"my_dir", ["foo", "bar"]}])
    end

    test "src not a src opts list" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > src` must be path, `{path, [opts]}`, `{:priv, path}`, `{:priv, path, [opts]}`, `{:system, path}`, `{:system, path, [opts]}`, or a list of those, got: `:moop`",
                   fn ->
                     make_module(otp_app: :zigler, c: [src: :moop])
                   end
    end

    test "path spec in src not a path" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > src` source files must be iodata, got: `:foo`",
                   fn -> make_module(otp_app: :zigler, c: [src: [{:foo, ["bar"]}]]) end
    end

    test "options in src not a string" do
      local_file = Path.join(__DIR__, "foo")

      assert_raise CompileError,
                   "test/options_test.exs:4: option `c > src > \"#{local_file}\"` must be a list of iodata, got: `[:bar]`",
                   fn -> make_module(otp_app: :zigler, c: [src: [{"foo", [:bar]}]]) end
    end
  end

  describe "the dir option" do
    test "accepts iodata" do
      local_path = Path.join(__DIR__, "foo/bar")
      assert %{dir: ^local_path} = make_module(otp_app: :zigler, dir: ["foo", "/" | "bar"])
    end

    test "rejects non-iodata" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `dir` must be a path, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, dir: :foo)
                   end
    end
  end

  describe "the easy_c option" do
    test "accepts iodata" do
      assert %{easy_c: "foo/bar"} = make_module(otp_app: :zigler, easy_c: ["foo", "/" | "bar"])
    end

    test "rejects non-iodata" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `easy_c` must be a path, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, easy_c: :foo)
                   end
    end
  end

  describe "optimize" do
    test "is ok with fast, small, safe, debug" do
      Enum.each(~w[fast small safe debug]a, fn mode ->
        assert %{optimize: ^mode} = make_module(otp_app: :zigler, optimize: mode)
      end)
    end

    test "rejects other" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `optimize` must be one of `:debug`, `:safe`, `:fast`, `:small`, `:env`, or `{:env, mode}`, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, optimize: :foo)
                   end
    end
  end

  describe "ignore" do
    test "is ok with a list of atoms" do
      assert %{ignore: [:foo, :bar]} = make_module(otp_app: :zigler, ignore: [:foo, :bar])
    end

    test "is ok with a single atom" do
      assert %{ignore: [:foo]} = make_module(otp_app: :zigler, ignore: :foo)
    end

    test "is not ok with something else" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `ignore` must be an atom or a list of atoms, got: `\"foo\"`",
                   fn ->
                     make_module(otp_app: :zigler, ignore: ["foo"])
                   end
    end
  end

  describe "extra_modules" do
    test "accepts name-path-deps" do
      assert %{extra_modules: [%{name: :foo, path: path, deps: [:baz, :quux]}]} =
               make_module(otp_app: :zigler, extra_modules: [foo: {"bar", [:baz, :quux]}])

      assert path == Path.expand("test/bar")
    end

    test "accepts dep-mod" do
      assert %{extra_modules: [%{dep: :dep, dst_mod: :foo, src_mod: :mymod}]} =
               make_module(
                 otp_app: :zigler,
                 dependencies: [dep: "path/to/dep"],
                 extra_modules: [foo: {:dep, :mymod}]
               )
    end

    test "dependency used in extra modules must be in dependencies" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `extra_modules > foo` requires the `dep` dependency, got: `[]`",
                   fn ->
                     make_module(otp_app: :zigler, extra_modules: [foo: {:dep, :mymod}])
                   end
    end

    test "path must be a string" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `extra_modules > foo` must be a tuple of the form `{path, [deps...]}`, got: `:bar` for path",
                   fn ->
                     make_module(otp_app: :zigler, extra_modules: [foo: {:bar, [:baz, :quux]}])
                   end
    end

    test "deps must be a list of atoms" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `extra_modules > foo` must be a list of atoms representing dependencies, got: `\"baz\"`",
                   fn ->
                     make_module(otp_app: :zigler, extra_modules: [foo: {"bar", "baz"}])
                   end
    end

    test "must be a keyword list" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `extra_modules` must be a list of module specifications, got: `\"foo\"`",
                   fn ->
                     make_module(otp_app: :zigler, extra_modules: "foo")
                   end

      assert_raise CompileError,
                   "test/options_test.exs:4: option `extra_modules` must be a list of module specifications, got: `\"bar\"`",
                   fn ->
                     make_module(otp_app: :zigler, extra_modules: ["bar"])
                   end
    end

    test "payload must be a tuple" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `extra_modules > foo` must be a tuple of the form `{path, [deps...]}`, got: `\"bar\"`",
                   fn ->
                     make_module(otp_app: :zigler, extra_modules: [foo: "bar"])
                   end
    end
  end

  describe "resources" do
    test "is ok with a list of atoms" do
      assert %{resources: [:foo, :bar]} = make_module(otp_app: :zigler, resources: [:foo, :bar])
    end

    test "is ok with a single atom" do
      assert %{resources: [:foo]} = make_module(otp_app: :zigler, resources: :foo)
    end

    test "is not ok with something else" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `resources` must be an atom or a list of atoms, got: `\"foo\"`",
                   fn ->
                     make_module(otp_app: :zigler, resources: ["foo"])
                   end
    end
  end

  describe "callbacks" do
    test "can have on_load as an atom" do
      assert %{callbacks: [on_load: :on_load]} =
               make_module(otp_app: :zigler, callbacks: [:on_load])
    end

    test "on_load can be keyword atom" do
      assert %{callbacks: [on_load: :fun]} =
               make_module(otp_app: :zigler, callbacks: [on_load: :fun])
    end

    test "on_load rejects anything else" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `callbacks > on_load` must be an atom, got: `[\"foo\"]`",
                   fn ->
                     make_module(otp_app: :zigler, callbacks: [on_load: ["foo"]])
                   end
    end

    test "can have on_upgrade as an atom" do
      assert %{callbacks: [on_upgrade: :on_upgrade]} =
               make_module(otp_app: :zigler, callbacks: [:on_upgrade])
    end

    test "on_upgrade can be keyword atom" do
      assert %{callbacks: [on_upgrade: :fun]} =
               make_module(otp_app: :zigler, callbacks: [on_upgrade: :fun])
    end

    test "on_upgrade rejects anything else" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `callbacks > on_upgrade` must be an atom, got: `[\"foo\"]`",
                   fn ->
                     make_module(otp_app: :zigler, callbacks: [on_upgrade: ["foo"]])
                   end
    end

    test "can have on_unload as an atom" do
      assert %{callbacks: [on_unload: :on_unload]} =
               make_module(otp_app: :zigler, callbacks: [:on_unload])
    end

    test "on_unload can be keyword atom" do
      assert %{callbacks: [on_unload: :fun]} =
               make_module(otp_app: :zigler, callbacks: [on_unload: :fun])
    end

    test "on_unload rejects anything else" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `callbacks > on_unload` must be an atom, got: `[\"foo\"]`",
                   fn ->
                     make_module(otp_app: :zigler, callbacks: [on_unload: ["foo"]])
                   end
    end

    test "anything else is not allowed" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `callbacks` must be a list of callback specifications, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, callbacks: :foo)
                   end
    end
  end

  describe "cleanup" do
    test "cleanup gets placed in default nif opts" do
      assert %{default_nif_opts: [cleanup: true]} = make_module(otp_app: :zigler, cleanup: true)
    end

    test "cleanup must be a boolean" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `cleanup` must be a boolean, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, cleanup: :foo)
                   end
    end
  end

  describe "leak_check" do
    test "leak_check gets placed in default nif opts" do
      assert %{default_nif_opts: [leak_check: true]} =
               make_module(otp_app: :zigler, leak_check: true)
    end

    test "leak_check must be a boolean" do
      assert_raise CompileError,
                   "test/options_test.exs:4: option `leak_check` must be a boolean, got: `:foo`",
                   fn ->
                     make_module(otp_app: :zigler, leak_check: :foo)
                   end
    end
  end

  test "an invalid key" do
    assert_raise CompileError,
                 "test/options_test.exs:4: `use Zig` was supplied the invalid option `foo`",
                 fn ->
                   make_module(otp_app: :zigler, foo: :bar)
                 end
  end
end
